import asyncio
import logging
from functools import reduce

from typing import Text, Optional, List, Dict

from rasa.nlu.training_data import TrainingData

from rasa.core.domain import Domain
from rasa.core.interpreter import RegexInterpreter, NaturalLanguageInterpreter
from rasa.core.training.structures import StoryGraph

import rasa.utils.io as io_utils
import rasa.utils.common as common_utils


logger = logging.getLogger(__name__)

"""
主要定义：
    -TrainingDataImporter接口类
        - NluDataImporter 继承类
        - CoreDataImporter 继承类
        - CombinedDataImporter 继承类
这个数据导入器挺有意思啊。。
"""


class TrainingDataImporter:
    """用于加载训练数据的不同机制的通用接口"""

    async def get_domain(self) -> Domain:
        """检索机器人的域
        Returns: 加载 ``Domain``.
        """
        raise NotImplementedError()

    async def get_config(self) -> Dict:
        """检索用于训练的配置
        Returns: 字典形式的配置.
        """
        raise NotImplementedError()

    async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData:
        """检索用于训练的NLU训练数据.
        Args:   language: 可用于仅加载特定语言的训练数据
        Returns: Loaded NLU ``TrainingData``: 加载的NLU训练数据
        """
        raise NotImplementedError()

    async def get_stories(
        self,
        interpreter: "NaturalLanguageInterpreter" = RegexInterpreter(),
        template_variables: Optional[Dict] = None,
        use_e2e: bool = False,
        exclusion_percentage: Optional[int] = None,
    ) -> StoryGraph:
        """检索用于训练的story（故事）.
        Args:
            interpreter: 解释器，用于解析端到端学习注释。
            template_variables: 读取故事文件时应替换的模板的值
            use_e2e: 指定是否解析端对端学习注释
            exclusion_percentage: 应当排除的训练数据量
        Returns:
            ``StoryGraph`` 包含所有已加载的故事
        """

        raise NotImplementedError()

    @staticmethod
    def load_from_config(
        config_path: Text,
        domain_path: Optional[Text] = None,
        training_data_paths: Optional[List[Text]] = None,
    ) -> "TrainingDataImporter":
        """从原始配置文件加载 “TrainingDataImporter” 实例"""

        # 调用read_config_file函数读取yaml文件，并以字典的形式返回文件内容
        config = io_utils.read_config_file(config_path)

        # 返回CombinedDataImporterr对象
        return TrainingDataImporter.load_from_dict(
            config, config_path, domain_path, training_data_paths
        )

    @staticmethod
    def load_core_importer_from_config(
        config_path: Text,
        domain_path: Optional[Text] = None,
        training_data_paths: Optional[List[Text]] = None,
    ) -> "TrainingDataImporter":
        """从配置文件中仅读取core训练数据部分以加载 “TrainingDataImporter” 实例"""

        importer = TrainingDataImporter.load_from_config(
            config_path, domain_path, training_data_paths
        )

        return CoreDataImporter(importer)

    @staticmethod
    def load_nlu_importer_from_config(
        config_path: Text,
        domain_path: Optional[Text] = None,
        training_data_paths: Optional[List[Text]] = None,
    ) -> "TrainingDataImporter":
        """从配置文件中仅读取NLU训练数据部分以加载 “TrainingDataImporter” 实例"""

        importer = TrainingDataImporter.load_from_config(
            config_path, domain_path, training_data_paths
        )

        return NluDataImporter(importer)

    @staticmethod
    def load_from_dict(
        config: Optional[Dict],
        config_path: Text,
        domain_path: Optional[Text] = None,
        training_data_paths: Optional[List[Text]] = None,
    ) -> "TrainingDataImporter":
        """从字典中（自定义的config中的"importers"键）加载 “TrainingDataImporter” 实例 """

        from rasa.importers.rasa import RasaFileImporter

        config = config or {}
        importers = config.get("importers", [])  # 数据导入器

        # 构建输入器对象列表，目前config.yml中"importers"是空的
        importers = [
            TrainingDataImporter._importer_from_dict(
                importer, config_path, domain_path, training_data_paths)
            for importer in importers
        ]
        importers = [importer for importer in importers if importer]

        # 输入器对象列表为空时，添加默认的输入器：RasaFileImporter
        if not importers:
            importers = [
                RasaFileImporter(config_path, domain_path, training_data_paths)
            ]

        return CombinedDataImporter(importers)  # 最后返回的必须是混合数据导入器，综合各个类型的导入器

    @staticmethod
    def _importer_from_dict(
        importer_config: Dict,
        config_path: Text,
        domain_path: Optional[Text] = None,
        training_data_paths: Optional[List[Text]] = None,
    ) -> Optional["TrainingDataImporter"]:

        from rasa.importers.multi_project import MultiProjectImporter
        from rasa.importers.rasa import RasaFileImporter

        module_path = importer_config.pop("name", None)
        if module_path == RasaFileImporter.__name__:
            importer_class = RasaFileImporter
        elif module_path == MultiProjectImporter.__name__:
            importer_class = MultiProjectImporter
        else:
            try:
                importer_class = common_utils.class_from_module_path(module_path)
            except (AttributeError, ImportError):
                logging.warning(f"Importer '{module_path}' not found.")
                return None

        constructor_arguments = common_utils.minimal_kwargs(
            importer_config, importer_class
        )
        return importer_class(
            config_path, domain_path, training_data_paths, **constructor_arguments
        )


class NluDataImporter(TrainingDataImporter):
    """跳过core相关的文件内容后的importer
    Importer that skips any Core-related file reading.
    """

    def __init__(self, actual_importer: TrainingDataImporter):
        self._importer = actual_importer

    async def get_domain(self) -> Domain:
        return Domain.empty()

    async def get_stories(
        self,
        interpreter: "NaturalLanguageInterpreter" = RegexInterpreter(),
        template_variables: Optional[Dict] = None,
        use_e2e: bool = False,
        exclusion_percentage: Optional[int] = None,
    ) -> StoryGraph:
        return StoryGraph([])

    async def get_config(self) -> Dict:
        return await self._importer.get_config()

    async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData:
        return await self._importer.get_nlu_data(language)


class CoreDataImporter(TrainingDataImporter):
    """core文件导入器：跳过任何与NLU相关的文件读取的importer"""

    def __init__(self, actual_importer: TrainingDataImporter):
        self._importer = actual_importer

    async def get_domain(self) -> Domain:
        return await self._importer.get_domain()

    async def get_stories(
        self,
        interpreter: "NaturalLanguageInterpreter" = RegexInterpreter(),
        template_variables: Optional[Dict] = None,
        use_e2e: bool = False,
        exclusion_percentage: Optional[int] = None,
    ) -> StoryGraph:
        return await self._importer.get_stories(
            interpreter, template_variables, use_e2e, exclusion_percentage
        )

    async def get_config(self) -> Dict:
        return await self._importer.get_config()

    async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData:
        return TrainingData()


class CombinedDataImporter(TrainingDataImporter):
    """一个 "TrainingDataImporter"，支持使用多个``TrainingDataImporter''，就好像它们是单个实例一样

    这里是主要读取训练数据的函数，采用协程读取config、domain、stories、nlu_data等
    """

    def __init__(self, importers: List[TrainingDataImporter]):
        self._importers = importers

    async def get_config(self) -> Dict:
        configs = [importer.get_config() for importer in self._importers]
        configs = await asyncio.gather(*configs)

        return reduce(lambda merged, other: {**merged, **(other or {})}, configs, {})

    async def get_domain(self) -> Domain:
        domains = [importer.get_domain() for importer in self._importers]
        domains = await asyncio.gather(*domains)  # 异步

        return reduce(
            lambda merged, other: merged.merge(other), domains, Domain.empty()
        )

    async def get_stories(
        self,
        interpreter: "NaturalLanguageInterpreter" = RegexInterpreter(),
        template_variables: Optional[Dict] = None,
        use_e2e: bool = False,
        exclusion_percentage: Optional[int] = None,
    ) -> StoryGraph:
        stories = [
            importer.get_stories(
                interpreter, template_variables, use_e2e, exclusion_percentage
            )
            for importer in self._importers
        ]
        stories = await asyncio.gather(*stories)

        return reduce(
            lambda merged, other: merged.merge(other), stories, StoryGraph([])
        )

    async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData:
        nlu_data = [importer.get_nlu_data(language) for importer in self._importers]
        nlu_data = await asyncio.gather(*nlu_data)  # 协程异步读取nlu数据

        return reduce(
            lambda merged, other: merged.merge(other), nlu_data, TrainingData()
        )
